module ActiveMerchant
  module Billing
    class PlugnpayGateway < Gateway
      class PlugnpayPostData < PostData
        # Fields that will be sent even if they are blank
        self.required_fields = [ :publisher_name, :publisher_password,
          :card_amount, :card_name, :card_number, :card_exp, :orderID ]
      end
      self.live_url = self.test_url = 'https://pay1.plugnpay.com/payment/pnpremote.cgi'

      CARD_CODE_MESSAGES = {
        "M" => "Card verification number matched",
        "N" => "Card verification number didn't match",
        "P" => "Card verification number was not processed",
        "S" => "Card verification number should be on card but was not indicated",
        "U" => "Issuer was not certified for card verification"
      }

      CARD_CODE_ERRORS = %w( N S )

      AVS_MESSAGES = {
        "A" => "Street address matches billing information, zip/postal code does not",
        "B" => "Address information not provided for address verification check",
        "E" => "Address verification service error",
        "G" => "Non-U.S. card-issuing bank",
        "N" => "Neither street address nor zip/postal match billing information",
        "P" => "Address verification not applicable for this transaction",
        "R" => "Payment gateway was unavailable or timed out",
        "S" => "Address verification service not supported by issuer",
        "U" => "Address information is unavailable",
        "W" => "9-digit zip/postal code matches billing information, street address does not",
        "X" => "Street address and 9-digit zip/postal code matches billing information",
        "Y" => "Street address and 5-digit zip/postal code matches billing information",
        "Z" => "5-digit zip/postal code matches billing information, street address does not",
      }

      AVS_ERRORS = %w( A E N R W Z )

      PAYMENT_GATEWAY_RESPONSES = {
        "P01" => "AVS Mismatch Failure",
        "P02" => "CVV2 Mismatch Failure",
        "P21" => "Transaction may not be marked",
        "P30" => "Test Tran. Bad Card",
        "P35" => "Test Tran. Problem",
        "P40" => "Username already exists",
        "P41" => "Username is blank",
        "P50" => "Fraud Screen Failure",
        "P51" => "Missing PIN Code",
        "P52" => "Invalid Bank Acct. No.",
        "P53" => "Invalid Bank Routing No.",
        "P54" => "Invalid/Missing Check No.",
        "P55" => "Invalid Credit Card No.",
        "P56" => "Invalid CVV2/CVC2 No.",
        "P57" => "Expired. CC Exp. Date",
        "P58" => "Missing Data",
        "P59" => "Missing Email Address",
        "P60" => "Zip Code does not match Billing State.",
        "P61" => "Invalid Billing Zip Code",
        "P62" => "Zip Code does not match Shipping State.",
        "P63" => "Invalid Shipping Zip Code",
        "P64" => "Invalid Credit Card CVV2/CVC2 Format.",
        "P65" => "Maximum number of attempts has been exceeded.",
        "P66" => "Credit Card number has been flagged and can not be used to access this service.",
        "P67" => "IP Address is on Blocked List.",
        "P68" => "Billing country does not match ipaddress country.",
        "P69" => "US based ipaddresses are currently blocked.",
        "P70" => "Credit Cards issued from this bank are currently not being accepted.",
        "P71" => "Credit Cards issued from this bank are currently not being accepted.",
        "P72" => "Daily volume exceeded.",
        "P73" => "Too many transactions within allotted time.",
        "P91" => "Missing/incorrect password",
        "P92" => "Account not configured for mobil administration",
        "P93" => "IP Not registered to username.",
        "P94" => "Mode not permitted for this account.",
        "P95" => "Currently Blank",
        "P96" => "Currently Blank",
        "P97" => "Processor not responding",
        "P98" => "Missing merchant/publisher name",
        "P99" => "Currently Blank"
      }

      TRANSACTIONS = {
        :authorization => 'auth',
        :purchase => 'auth',
        :capture => 'mark',
        :void => 'void',
        :refund => 'return',
        :credit => 'newreturn'
      }

      SUCCESS_CODES = [ 'pending', 'success' ]
      FAILURE_CODES = [ 'badcard', 'fraud' ]

      self.default_currency = 'USD'
      self.supported_countries = ['US']
      self.supported_cardtypes = [:visa, :master, :american_express, :discover]
      self.homepage_url = 'http://www.plugnpay.com/'
      self.display_name = "Plug'n Pay"

      def initialize(options = {})
        requires!(options, :login, :password)
        super
      end

      def purchase(money, creditcard, options = {})
        post = PlugnpayPostData.new

        add_amount(post, money, options)
        add_creditcard(post, creditcard)
        add_addresses(post, options)
        add_invoice_data(post, options)
        add_customer_data(post, options)

        post[:authtype] = 'authpostauth'
        commit(:authorization, post)
      end

      def authorize(money, creditcard, options = {})
        post = PlugnpayPostData.new

        add_amount(post, money, options)
        add_creditcard(post, creditcard)
        add_addresses(post, options)
        add_invoice_data(post, options)
        add_customer_data(post, options)

        post[:authtype] = 'authonly'
        commit(:authorization, post)
      end

      def capture(money, authorization, options = {})
        post = PlugnpayPostData.new

        post[:orderID] = authorization

        add_amount(post, money, options)
        add_customer_data(post, options)

        commit(:capture, post)
      end

      def void(authorization, options = {})
        post = PlugnpayPostData.new

        post[:orderID] = authorization
        post[:txn_type] = 'auth'

        commit(:void, post)
      end

      def credit(money, identification_or_creditcard, options = {})
        post = PlugnpayPostData.new
        add_amount(post, money, options)

        if identification_or_creditcard.is_a?(String)
          ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
          refund(money, identification_or_creditcard, options)
        else
          add_creditcard(post, identification_or_creditcard)
          add_addresses(post, options)
          add_customer_data(post, options)

          commit(:credit, post)
        end
      end

      def refund(money, reference, options = {})
        post = PlugnpayPostData.new
        add_amount(post, money, options)
        post[:orderID] = reference
        commit(:refund, post)
      end

      private
      def commit(action, post)
        response = parse( ssl_post(self.live_url, post_data(action, post)) )
        success = SUCCESS_CODES.include?(response[:finalstatus])
        message = success ? 'Success' : message_from(response)

        Response.new(success, message, response,
          :test => test?,
          :authorization => response[:orderid],
          :avs_result => { :code => response[:avs_code] },
          :cvv_result => response[:cvvresp]
        )
      end

      def parse(body)
        body = CGI.unescape(body)
        results = {}
        body.split('&').collect { |e| e.split('=') }.each do |key,value|
          results[key.downcase.to_sym] = normalize(value.to_s.strip)
        end

        results.delete(:publisher_password)
        results[:avs_message] = AVS_MESSAGES[results[:avs_code]] if results[:avs_code]
        results[:card_code_message] = CARD_CODE_MESSAGES[results[:cvvresp]] if results[:cvvresp]

        results
      end

      def post_data(action, post)
        post[:mode]               = TRANSACTIONS[action]
        post[:convert]            = 'underscores'
        post[:app_level]          = 0
        post[:publisher_name]     = @options[:login]
        post[:publisher_password] = @options[:password]

        post.to_s
      end

      def add_creditcard(post, creditcard)
        post[:card_number]  = creditcard.number
        post[:card_cvv]     = creditcard.verification_value
        post[:card_exp]     = expdate(creditcard)
        post[:card_name]    = creditcard.name.slice(0..38)
      end

      def add_customer_data(post, options)
        post[:email] = options[:email]
        post[:dontsndmail]        = 'yes' unless options[:send_email_confirmation]
        post[:ipaddress] = options[:ip]
      end

      def add_invoice_data(post, options)
        post[:shipping] = amount(options[:shipping]) unless options[:shipping].blank?
        post[:tax] = amount(options[:tax]) unless options[:tax].blank?
      end

      def add_addresses(post, options)
        if address = options[:billing_address] || options[:address]
          post[:card_address1] = address[:address1]
          post[:card_zip]      = address[:zip]
          post[:card_city]     = address[:city]
          post[:card_country]  = address[:country]
          post[:phone]         = address[:phone]

          case address[:country]
          when 'US', 'CA'
            post[:card_state] = address[:state]
          else
            post[:card_state] = 'ZZ'
            post[:card_prov]  = address[:state]
          end
        end

        if shipping_address = options[:shipping_address] || address
          post[:shipname] = shipping_address[:name]
          post[:address1] = shipping_address[:address1]
          post[:address2] = shipping_address[:address2]
          post[:city] = shipping_address[:city]

          case shipping_address[:country]
          when 'US', 'CA'
            post[:state] = shipping_address[:state]
          else
            post[:state] = 'ZZ'
            post[:province]  = shipping_address[:state]
          end

          post[:country] = shipping_address[:country]
          post[:zip] = shipping_address[:zip]
        end
      end

      def add_amount(post, money, options)
        post[:card_amount] = amount(money)
        post[:currency] = options[:currency] || currency(money)
      end

      def message_from(results)
        PAYMENT_GATEWAY_RESPONSES[results[:resp_code]]
      end

      def expdate(creditcard)
        year  = sprintf("%.4i", creditcard.year)
        month = sprintf("%.2i", creditcard.month)

        "#{month}/#{year[-2..-1]}"
      end
    end
  end
end
